#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h> /* register_chrdev_region(), struct file_operations */
#include <linux/slab.h> /* kmalloc() */
#include <linux/capability.h>
#include <asm/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include "scull.h"
#include "pipe.h" /* pipe device */

MODULE_LICENSE("Dual BSD/GPL");

static unsigned int major_number = 0;
static unsigned int minor_number = 0;
static unsigned int number_devices = NUM_DEV;
static unsigned int quantum = QUANTUM; /* розмір обл. пам'яті */
static unsigned int quantum_num = QUANTUM_NUM; /* кількість обл. пам'яті */

struct ScullDevice *scull_devices;

static struct file_operations f_ops = {
    .owner = THIS_MODULE,
    .read = read,
    .write = write,
    .open = open,
    .llseek = seek,
    .unlocked_ioctl = ioctl,
};

void freeMem(struct ScullDevice *scull_dev)
{
    struct ScullInternalDevice *next, *curr;
    int i;
    unsigned q_num = scull_dev->quantum_num;
    for (curr = scull_dev->internal_device; curr; curr = next) {
        if (curr->data)
        {
            for (i = 0; i < q_num; i++)
                if (curr->data[i])
                    kfree(curr->data[i]);
            kfree(curr->data);
        }
        next = curr->next;
        kfree(curr);
    }
}

struct ScullInternalDevice *getInternalDevice(struct ScullDevice *dev, unsigned index)
{
    struct ScullInternalDevice *internal_dev = dev->internal_device;
    if (!internal_dev)
    {
        internal_dev = dev->internal_device = kmalloc(sizeof(struct ScullInternalDevice), GFP_KERNEL);
        if (internal_dev == NULL)
            return NULL;
        memset(internal_dev, 0, sizeof(struct ScullInternalDevice));
    }
    while (index--) {
        if (!internal_dev->next)
        {
            internal_dev->next = kmalloc(sizeof(struct ScullInternalDevice), GFP_KERNEL);
            if (internal_dev->next == NULL)
                return NULL;
            memset(internal_dev->next, 0, sizeof(struct ScullInternalDevice));
        }
        internal_dev = internal_dev->next;
    }
    return internal_dev;
}

long ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    if (DEVICE_COMMAND != _IOC_TYPE(cmd))
        return -ENOTTY;
    if (_IOC_NR(cmd) > IOC_MAX_NUM)
        return -ENOTTY;

    struct ScullDevice *dev = f->private_data;

    int retval;
    if (_IOC_DIR(cmd) & _IOC_READ)
        retval = access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
    else if (_IOC_DIR(cmd) & _IOC_WRITE)
        retval = access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
    if (!retval)
        return -EFAULT;
    switch (cmd) {
    case IOC_RESSET :
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        dev->quantum = quantum;
        dev->quantum_num = quantum_num;
        break;
    case IOC_S_QUANTUM :
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        retval = __get_user(dev->quantum, (int __user *)arg);
        break;
    case IOC_S_QSET :
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        retval = __get_user(dev->quantum_num, (int __user *)arg);
        break;
    case IOC_G_QUANTUM :
        retval = __put_user(dev->quantum, (int __user *)arg);
        break;
    case IOC_G_QSET :
        retval = __put_user(dev->quantum_num, (int __user *)arg);
        break;
    case IOC_T_QUANTUM :
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        dev->quantum = arg;
        break;
    case IOC_T_QSET :
        if (!capable(CAP_SYS_ADMIN))
            return -EPERM;
        dev->quantum_num = arg;
        break;
    case IOC_Q_QUANTUM :
        return dev->quantum;
    case IOC_Q_QSET :
        return dev->quantum_num;
    default :
        return -ENOTTY;
    }
    return retval;
}

loff_t seek(struct file *f, int pos, int type)
{
    struct ScullDevice *dev = f->private_data;
    loff_t new_pos;
    switch (type) {
    case 0 : /* set new pos */
        new_pos = pos;
        break;
    case 1 : /* additional to current pos */
        new_pos = f->f_pos + pos;
        break;
    case 2 : /* set to end */
        new_pos = f->f_pos + dev->size;
        break;
    default:
        return -EINVAL;
    }
    if (pos < 0) return -EINVAL;
    f->f_pos = new_pos;
    return new_pos;
}

ssize_t write(struct file *f, const char __user *buff, size_t count, loff_t *pos)
{
    int retval = 0;
    struct ScullDevice *dev = f->private_data;
    unsigned node_size = dev->quantum * dev->quantum_num;
    unsigned node_index = (long)*pos / node_size;
    unsigned rest = (long)*pos % node_size;
    unsigned data_index = rest / dev->quantum;
    unsigned offset = rest % dev->quantum;
    if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
    struct ScullInternalDevice *d = getInternalDevice(dev, node_index);
    if (d == NULL)
        goto out;
    if (!d->data)
    {
        d->data = kmalloc(dev->quantum_num * sizeof(char*), GFP_KERNEL);
        if (!d->data)
            goto out;
        memset(d->data, 0, dev->quantum_num * sizeof(char*));
    }
    if (!d->data[data_index])
    {
        d->data[data_index] = kmalloc(dev->quantum, GFP_KERNEL);
        if (!d->data[data_index])
            goto out;
        memset(d->data[data_index], 0, dev->quantum);
    }
    if (offset + count > dev->quantum) /* пишем за один раз не більше quantum */
        count = dev->quantum - offset;
    if (copy_from_user(d->data[data_index] + offset, buff, count))
    {
        retval = -EFAULT;
        goto out;
    }
    *pos += count;
    retval = count;
out:
    up(&dev->sem);
    return retval;
}

ssize_t read(struct file *f,  char __user *buff, size_t count, loff_t *pos)
{printk("scull read() was called\n");
    int retval = 0;
    struct ScullDevice *dev = f->private_data;
    /* знаходимо потрібний node, потім область пам'яті та зміщення */
    unsigned node_size = dev->quantum * dev->quantum_num;
    unsigned node_index = (long)*pos / node_size;
    unsigned rest = (long)*pos % node_size;
    unsigned data_index = rest / dev->quantum;
    unsigned offset = rest % dev->quantum;
    if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
    struct ScullInternalDevice *d = getInternalDevice(dev, node_index);
    if (d == NULL || !d->data || !d->data[data_index])
        goto out;
    if (offset + count > dev->quantum)
        count = dev->quantum - offset;
    if (copy_to_user(buff, d->data[data_index] + offset, count))
    {
        retval = -EFAULT;
        goto out;
    }
    *pos += count;
    retval = count;
out:
    up(&dev->sem);
    return retval;
}

int open(struct inode *i, struct file *f)
{
    /* вибираємо з struct inode конкретний девайс з яким будемо працювати */
    struct ScullDevice *dev = container_of(i->i_cdev, struct ScullDevice, cdev);
    f->private_data = dev; /* для інших функцій (тому що в інші ф-ції не передається struct inode) */
    /*if ((f->f_flags & O_ACCMODE) == O_WRONLY)
    {
        if (down_interruptible(&dev->sem))
            return -ERESTARTSYS;
        freeMem(dev);
        up(&dev->sem);
    }*/
    return 0;
}

int cdevSetup(struct ScullDevice *scull_dev, int index)
{
    int result = 0;
    dev_t dev_num = MKDEV(major_number, minor_number + index);
    cdev_init(&scull_dev->cdev, &f_ops);
    scull_dev->cdev.owner = THIS_MODULE;
    scull_dev->cdev.ops = &f_ops;
    result = cdev_add(&scull_dev->cdev, dev_num, 1); /* реєстрація девайса в ядрі */
    return result;
}

/* повертаємо об'єкт ітератор */
static void *seq_start(struct seq_file *s, loff_t *pos)
{
    if (*pos >= number_devices)
        return NULL;
    return *pos + scull_devices;
}

static void *seq_next(struct seq_file *s, void *v, loff_t *pos)
{
    (*pos)++;
    if (*pos >= number_devices)
        return NULL;
    return *pos + scull_devices;
}

static void seq_stop(struct seq_file *s, void *v)
{
}

static int seq_show(struct seq_file *s, void *v)
{
    struct ScullDevice *dev = (struct ScullDevice*)(v);
    if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;
    seq_printf(s, "Device %i: quantum num - %i, quantum - %i\n", (int)(dev - scull_devices), dev->quantum_num, dev->quantum);
    struct ScullInternalDevice *d;
    unsigned i;
    for (d = dev->internal_device; d; d = d->next) {
        seq_printf(s, "\tInternal Device - %p, data - %p\n", d, d->data);
        /* виводимо інформацію про останній ScullInternalDevice */
        if (d->data && !d->next)
        {
            for (i = 0; i < dev->quantum_num; i++)
                if (d->data[i])
                    seq_printf(s, "\t\tquantum num - %i, quantum %p\n", i, d->data[i]);
        }
    }
    up(&dev->sem);
    return 0;
}

struct seq_operations seq_fops = {
    .start = seq_start,
    .next = seq_next,
    .stop = seq_stop,
    .show = seq_show,
};

int proc_open(struct inode *i, struct file *f)
{
    seq_open(f, &seq_fops);
}

struct file_operations f_seq_ops = {
    .owner = THIS_MODULE,
    .open = proc_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = seq_release,
};

void createProcEntry()
{
    struct proc_dir_entry *proc_entry = create_proc_entry("scull", 0, NULL);
    if (proc_entry)
        proc_entry->proc_fops = &f_seq_ops;
}

static void scullExit(void)
{
    dev_t dev_num = MKDEV(major_number, minor_number);
    unregister_chrdev_region(dev_num, number_devices);
    int i;
    if (scull_devices)
    {
        for (i = 0; i < number_devices; i++) {
            cdev_del(&scull_devices[i].cdev);
            freeMem(scull_devices + i);
        }
        kfree(scull_devices);
    }
    remove_proc_entry("scull", NULL);
    clearPipeDevices();
}

static int scullInit(void)
{
    int result;
    dev_t dev_num; /* номер символьного девайса */
    if (major_number) /* параметри вказані статично */
    {
        dev_num = MKDEV(major_number, minor_number);
        result = register_chrdev_region(dev_num, number_devices, "scull");
    }
    else /* виділяємо параметри динамічно */
    {
        result = alloc_chrdev_region(&dev_num, minor_number, number_devices, "scull");
        major_number = MAJOR(dev_num);
    }
    if (result < 0) goto out;

    scull_devices = kmalloc(number_devices * sizeof(struct ScullDevice), GFP_KERNEL);
    if (!scull_devices)
    {
        result = -ENOMEM;
        goto out;
    }
    memset(scull_devices, 0, number_devices * sizeof(struct ScullDevice));

    /* ініціалізація scull_device та cdev*/
    int i;
    for (i = 0; i < number_devices; i++) {
        scull_devices[i].quantum = quantum;
        scull_devices[i].quantum_num = quantum_num;
        sema_init(&scull_devices[i].sem, 1);
        cdevSetup(scull_devices + i, i);
    }

    createProcEntry();

    dev_num = MKDEV(major_number, minor_number + number_devices);

    initPipeDevices(dev_num);

    return 0;

out:
    scullExit();
    return result;
}

module_init(scullInit);
module_exit(scullExit);
